package de.lighti.starcraft;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Logger;

import de.lighti.starcraft.Server.TOURNAMENT_TYPE;
import de.lighti.starcraft.model.Match;
import de.lighti.starcraft.model.StarcraftBot;
import de.lighti.starcraft.model.StarcraftMap;
import de.lighti.starcraft.network.ByteBufferBackedInputStream;
import de.lighti.starcraft.util.UtilFunctions;

public class GameDatabase {
    private final static String RESULTS_FILE = "rawData.txt";

    private static final Logger LOGGER = Logger.getLogger( GameDatabase.class.getName() );

    private SortedMap<Integer, Vector<Match>> matches;

    private Queue<Match> gameQueue;
    private Set<Match> matchesInProgress;
    private Set<Match> finishedMatches;

    private BufferedWriter resultsStream;

    public GameDatabase() {
        try {
            final File resultsFile = new File( RESULTS_FILE );
            final boolean writeHeader = !resultsFile.exists();
            resultsStream = new BufferedWriter( new FileWriter( resultsFile, true ) );
            LOGGER.info( "Aquired lock on " + RESULTS_FILE );
            if (writeHeader) {
                resultsStream.write( Match.FIELDS_HEADER );
                resultsStream.flush();
            }
        }
        catch (final IOException e) {
            LOGGER.warning( "Can't open results file " + RESULTS_FILE + ". Will resort to console logging instead" );
        }
    }

    private void benchTournament( int firstGame, int rounds, Vector<StarcraftMap> maps, Vector<StarcraftBot> bots, Vector<StarcraftBot> benchBots ) {

        for (int i = 1; i <= rounds; i++) {
            final Vector<Match> roundMatches = new Vector<Match>();
            matches.put( i, roundMatches );

            for (final StarcraftMap m : maps) {

                for (final StarcraftBot bot : bots) {
                    for (final StarcraftBot bench : benchBots) {

                        final Match match = new Match( firstGame, i, bot, bench, m );
                        firstGame++;
                        roundMatches.add( match );

                    }
                }

            }
        }
    }

    private void clear() {
        matches = new TreeMap<Integer, Vector<Match>>();
        gameQueue = new LinkedBlockingQueue<Match>();
        matchesInProgress = new HashSet<Match>();
        finishedMatches = new HashSet<Match>();
    }

    public void createNew( TOURNAMENT_TYPE tournamentType, Vector<StarcraftBot> bots, Vector<StarcraftMap> maps, int rounds, int firstGameid ) {
        clear();

        final Vector<StarcraftBot> benchBots = new Vector<StarcraftBot>();
        final Vector<StarcraftBot> temp = new Vector<StarcraftBot>();
        for (final StarcraftBot b : bots) {
            if (b.isBenchmarkBot()) {
                benchBots.add( b );
            }
            else {
                temp.add( b );
            }
        }
        bots = temp;

        switch (tournamentType) {
            case benchMark:
                benchTournament( firstGameid, rounds, maps, bots, benchBots );
                break;
            case randomrobin:
                randomRoundRodin( firstGameid, rounds, maps, bots );
                break;
            case roundrobin:
                standardMapRoundRobin( firstGameid, rounds, maps, bots );
                break;
            default:
                throw new IllegalArgumentException( "unknown tournament type" );
        }
        int number = 0;
        for (final Vector<Match> v : matches.values()) {
            number += v.size();
            gameQueue.addAll( v );
        }
        LOGGER.info( "Generated at total of " + number + " matches in " + matches.size() + " rounds for tournament type " + tournamentType );
    }

    public void dumpGamesList( File gamesList ) throws IOException {
        if (gamesList == null || !gamesList.canWrite()) {
            throw new IllegalArgumentException( "Can't write to file " + (gamesList != null ? gamesList.getName() : "<null>") );
        }

        final StringBuffer buffer = new StringBuffer();
        buffer.append( "##Starcraft Tournament list. Generated " + new Date() + "\n" );
        buffer.append( "\n" );
        int id = 0;
        for (final Map.Entry<Integer, Vector<Match>> e : matches.entrySet()) {
            buffer.append( "## Round " + e.getKey() + "\n" );
            for (final Match m : e.getValue()) {
                buffer.append( "\t" + id + "\t" + e.getKey() + "\t" + m.getHomeBot().getName() + "\t" + m.getAwayBot().getName() + "\t" + m.getMap().getName()
                                + "\n" );
                id++;
            }
        }
        final FileWriter w = new FileWriter( gamesList );
        w.write( buffer.toString() );
        w.close();

    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();

        if (resultsStream != null) {
            resultsStream.close();
            LOGGER.info( "Closing " + RESULTS_FILE );
        }
    }

    public int getClientsNeeded() {
        if (gameQueue.isEmpty()) {
            return 0;
        }
        final Match m = gameQueue.peek();
        int ret = 0;
        if (!m.getHomeBot().isBenchmarkBot()) {
            ret++;
        }
        if (!m.getAwayBot().isBenchmarkBot()) {
            ret++;
        }
        return ret;
    }

    public Match getMatchInProgess( int gameID ) {
        for (final Match m : matchesInProgress) {
            if (m.getGameID() == gameID) {
                return m;
            }
        }

        return null;
    }

    public Match getNextMatch() {
        return gameQueue.poll();
    }

    public void loadBotFromFile( StarcraftBot s ) throws FileNotFoundException, IOException {
        final File f = new File( "..\\Bots\\" + s.getName() + "\\bot\\" + s.getName() + ".dll" );
        final ByteBuffer buffer = ByteBufferBackedInputStream.readStreamInputBuffer( new FileInputStream( f ) );
        final HashMap<String, ByteBuffer> map = new HashMap<>();
        map.put( s.getName() + ".dll", buffer );
        s.setBinaryContent( map );

    }

    public void loadFromFile( int firstGameToPlay, File gamesList, Vector<StarcraftBot> bots, List<StarcraftMap> maps ) throws IOException {
        if (gamesList == null || !gamesList.canRead()) {
            throw new IllegalArgumentException( "Can't read file " + gamesList != null ? gamesList.getAbsolutePath() : "<null>" );
        }
        clear();

        BufferedReader reader = null;
        try {
            reader = new BufferedReader( new FileReader( gamesList ) );
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty() || line.startsWith( "#" )) {
                    continue;
                }
                try {
                    final StringTokenizer tokens = new StringTokenizer( line );
                    final Integer id = Integer.parseInt( tokens.nextToken() );
                    final Integer round = Integer.parseInt( tokens.nextToken() );
                    final String one = tokens.nextToken();
                    final String two = tokens.nextToken();
                    final String mapString = tokens.nextToken();
                    final StarcraftBot botOne = UtilFunctions.findBotInCollection( bots, one );
                    final StarcraftBot botTwo = UtilFunctions.findBotInCollection( bots, two );
                    final StarcraftMap map = UtilFunctions.findMapInCollection( maps, mapString );
                    if (botOne == null || botTwo == null || map == null) {
                        LOGGER.warning( gamesList.getName() + " contains invalid match entry: " + line );
                        continue;
                    }
                    if (!matches.containsKey( round )) {
                        matches.put( round, new Vector<Match>() );
                    }
                    final Vector<Match> roundMatches = matches.get( round );
                    roundMatches.add( new Match( id, round, botOne, botTwo, map ) );
                }
                catch (final NoSuchElementException | NumberFormatException e) {
                    LOGGER.warning( gamesList.getName() + " contains invalid match entry: " + line );
                }
            }
            int number = 0;
            for (final Vector<Match> v : matches.values()) {
                number += v.size();
                for (final Match m : v) {
                    if (m.getGameID() >= firstGameToPlay) {
                        gameQueue.add( m );
                    }
                }
            }
            LOGGER.info( "Restored at total of " + number + " matches in " + matches.size() + " rounds from disk" );
        }
        catch (final IOException e) {
            throw e;
        }
        finally {
            if (reader != null) {
                reader.close();
            }
        }

    }

    public void loadMapFromFile( StarcraftMap s ) throws IOException {
        final File f = new File( "..\\" + s.getMapLocation() );
        final ByteBuffer buffer = ByteBufferBackedInputStream.readStreamInputBuffer( new FileInputStream( f ) );
        ByteBuffer bwtaBuffer = null;
        if (s.getBWTAHash() != null) {
            final File bwta = new File( "..\\Maps\\" + s.getBWTAHash() + ".bwta" );
            bwtaBuffer = ByteBufferBackedInputStream.readStreamInputBuffer( new FileInputStream( bwta ) );
        }
        if (bwtaBuffer != null) {
            s.setBinaryContent( new ByteBuffer[] { buffer, bwtaBuffer } );
        }
        else {
            s.setBinaryContent( new ByteBuffer[] { buffer } );
        }

    }

    /**
     * @param rounds
     * @param maps
     * @param bots
     * @param gameID
     * @param out
     * @throws IOException
     */
    private void randomRoundRodin( int firstGameid, int rounds, Vector<StarcraftMap> maps, Vector<StarcraftBot> bots ) {

        final Random[][] pickerArray = new Random[bots.size()][bots.size()];
        for (int i = 0; i < bots.size(); i++) {
            for (int j = 0; j < bots.size(); j++) {
                if (i != j) {
                    final long seed = bots.get( j ).getRandomHex() ^ bots.get( i ).getRandomHex();
                    pickerArray[i][j] = new Random( seed );
                }
            }
        }

        for (int i = 1; i <= rounds; i++) {
            final Vector<Match> roundMatches = new Vector<Match>();
            matches.put( i, roundMatches );
            for (int j = 0; j < bots.size(); j++) {
                final StarcraftBot one = bots.get( j );

                for (int k = 0; k < bots.size(); k++) {
                    final StarcraftBot two = bots.get( k );

                    final int mapID = pickerArray[j][k].nextInt( maps.size() );
                    final StarcraftMap map = maps.get( mapID );

                    if (i % 2 == 0) {
                        if (k > j) {
                            roundMatches.add( new Match( firstGameid, i, one, two, map ) );
                            firstGameid++;
                        }
                    }
                    else {
                        if (k < j) {
                            roundMatches.add( firstGameid, new Match( firstGameid, i, one, two, map ) );
                            firstGameid++;
                        }
                    }
                }
            }
        }
    }

    public void reschedule( Match m ) {
        m.reset();
        matchesInProgress.remove( m );
        finishedMatches.remove( m );
        gameQueue.add( m );
    }

    public void setFinished( Match m ) {
        gameQueue.remove( m );
        matchesInProgress.remove( m );
        finishedMatches.add( m );

        if (resultsStream != null) {
            try {
                resultsStream.write( m.toString() );
                resultsStream.flush();
                LOGGER.info( "Written results for match " + m.getGameID() + " in round " + m.getRound() + " to disk." );
            }
            catch (final IOException e) {
                LOGGER.warning( "Unable to write to results file " + RESULTS_FILE );
                System.out.println( m );
            }
        }
        else {
            System.out.println( m );
        }

        if (m.getReplay() != null) {
            final String replayPath = "..\\Replays\\" + m.getGameID() + "-" + m.getHomeBot().getName() + "_vs_" + m.getAwayBot().getName() + ".rep";
            final File f = new File( replayPath );
            if (f.exists()) {
                LOGGER.info( "Already got replay for game " + m.getGameID() );
            }
            else {

                LOGGER.info( "Writing replay for game " + m.getGameID() + " to " + f.getAbsolutePath() );
                try {
                    f.createNewFile();
                    final FileOutputStream fw = new FileOutputStream( f );
                    m.getReplay().rewind();
                    fw.write( m.getReplay().array() );
                    fw.close();
                }
                catch (final IOException e) {
                    LOGGER.warning( "Failed wo write replay to " + replayPath + ": " + e.getLocalizedMessage() );
                }
            }

        }
    }

    public void setInProgress( Match m ) {
        gameQueue.remove( m );
        matchesInProgress.add( m );
    }

    private void standardMapRoundRobin( int firstGameId, int rounds, Vector<StarcraftMap> maps, Vector<StarcraftBot> bots ) {
        int roundNum = 1;

        for (int i = 1; i <= rounds; i++) {
            for (final StarcraftMap map : maps) {
                final Vector<Match> roundMatches = new Vector<Match>();
                matches.put( i, roundMatches );
                for (int j = 0; j < bots.size(); j++) {
                    final StarcraftBot one = bots.get( j );

                    for (int k = 0; k < bots.size(); k++) {
                        final StarcraftBot two = bots.get( k );

                        if (roundNum % 2 == 0) {

                            if (k > j) {
                                roundMatches.add( new Match( firstGameId, i, one, two, map ) );
                                firstGameId++;
                            }
                            else {
                                if (k < j) {
                                    roundMatches.add( new Match( firstGameId, i, one, two, map ) );
                                    firstGameId++;
                                }
                            }
                        }
                    }
                    roundNum++;
                }
            }
        }
    }
}
